/**
 * @(#)ContextoBundleTraductor.java 20/04/2010
 *
 * Copyright (c) 2007-2012 PsiqueWare S.A. de C.V. All Rights Reserved.
 * This software is the proprietary information PsiqueWare S.A. de C.V.
 * Use is subject to license terms.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *
 * <br/>Usuario : AngelElectrico
 * <br/>Creaci&oacute;n: 20/04/2010 - 09:21:17 PM
 * <br/>
 * <br/>
 * <br/><t5>Cambios :</t5>
 * <br/><b>Fecha | Autor  |  Cambios</b>
 * <ul>
 * <li>20/04/2010 | <a href = "mailto:AngelElectrico@PsiqueWare.com">&Aacute;ngel El&eacute;ctrico</a> | Creaci&oacute;n del Objeto.</li>
 *
 * </ul>
 */

package com.psiqueware.alheli.i18n.traductor;

import com.psiqueware.alheli.i18n.Lenguaje;
import com.psiqueware.alheli.i18n.Traductor;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

/**
 * Contexto para un {@link ResourceBundle} combinado para utilizar como base del traductor.
 * que tiene diferentes classloaders.
 * <br/>Paquete Original: com.psiqueware.alheli.i18n.osgi
 * <br/>Nombre  Original: ContextoBundleTraductor
 * <br/>Oprima para ver : <a href="ContextoBundleTraductor.java.html"><i>C&oacute;digo Fuente</i> </a>
 *
 * @author <a href="mailto:AngelElectrico@PsiqueWare.com">AngelElectrico</a>
 * @version Revision: 1.1 20/04/2010 09:21:17 PM
 * @noinspection UnusedDeclaration
 */
public final class ContextoBundleTraductor
{

    /**
     * Nombre del contexto para el conjunto de Bundles que se esta representando.
     */
    private final String m_nombreContexto;

    /**
     * {@link File} con el directorio para almacenar las traducciones.
     */
    private final File m_directorioTraduccion;

    /**
     * Listado de {@link String}s que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     */
    private final List<String> m_baseNamesBundles = new ArrayList<String>();

    /**
     * Listado de {@link ClassLoader}s que servir&aacute; para cargar el bundle.
     */
    private final List<ClassLoader> m_loaders = new ArrayList<ClassLoader>();

    /**
     * Constructor de ContextoBundleTraductor.
     *
     * @param nombreContexto       contexto.
     * @param directorioTraduccion directorio donde se almacenar&aacute; la traducci&oacute;n.
     */
    public ContextoBundleTraductor(final String nombreContexto, final File directorioTraduccion)
    {
        m_nombreContexto = nombreContexto;
        m_directorioTraduccion = directorioTraduccion;
    }

    /**
     * Devuelve el {@link ResourceBundle} traducido.
     *
     * @param origen        {@link Locale} en el que se encuentra el {@link ResourceBundle} origen.
     * @param traductor     para realizar la traducci&oacute;n.
     * @param localeOrigen  {@link Locale} de origen.
     * @param localeDestino {@link Locale} destino.
     *
     * @return el {@link ResourceBundle} con la traducci&oacute;n.
     *
     * @throws IOException en caso de no poder almacenar la descarga.
     */
    public ResourceBundle getBundle(final ResourceBundle origen, final Traductor traductor, final Locale localeOrigen, final Locale localeDestino) throws IOException
    {
        final ResourceBundle salida;
        final File archivoTraduccion = getFile(localeDestino);
        if (archivoTraduccion.exists())
        {
            salida = cargarBundleDesdeProperties(origen, archivoTraduccion);
        }
        else
        {
            salida = cargarBundleTraducido(origen, traductor, localeOrigen, localeDestino);
        }

        return null == salida ? origen : salida;
    }

    /**
     * Carga el {@link ResourceBundle} desde el archivo de {@link Properties}.
     *
     * @param origen            {@link ResourceBundle} de origen.
     * @param archivoTraduccion a cargar.
     *
     * @return el {@link ResourceBundle} nuevo.
     *
     * @throws IOException en caso de no poder almacenar la descarga.
     */
    private ResourceBundle cargarBundleDesdeProperties(final ResourceBundle origen, final File archivoTraduccion) throws IOException
    {
        final Properties properties = new Properties();
        final FileInputStream fis = new FileInputStream(archivoTraduccion);
        properties.loadFromXML(fis);
        fis.close();

        final Map<String, String> mapaOrigen = mapear(origen);
        final Map<String, String> mapaDestino = new HashMap<String, String>();
        final Set<Object> keysProps = properties.keySet();
        for (Object keysProp : keysProps)
        {
            final String key = String.valueOf(keysProp);
            mapaDestino.put(key, properties.getProperty(key));
        }

        return new BundleTraducido(mapaOrigen, mapaDestino, archivoTraduccion);
    }

    /**
     * Devuelve el {@link ResourceBundle} traducido.
     *
     * @param origen        {@link Locale} en el que se encuentra el {@link ResourceBundle} origen.
     * @param traductor     para realizar la traducci&oacute;n.
     * @param localeOrigen  {@link Locale} de origen.
     * @param localeDestino {@link Locale} destino.
     *
     * @return el {@link ResourceBundle} con la traducci&oacute;n.
     *
     * @throws IOException en caso de no poder almacenar la descarga.
     */
    public ResourceBundle cargarBundleTraducido(final ResourceBundle origen, final Traductor traductor, final Locale localeOrigen, final Locale localeDestino) throws IOException
    {
        //Este mapa no contiene retornos de carro, podemos asumir
        //que el orden antes y despu&eacute;s de traducir ser&aacute; el mismo.
        final Map<String, String> mapaOrigen = mapear(origen);

        //Generamos la lista de llaves y la de elementos a traducir
        final List<String> llaves = new ArrayList<String>(mapaOrigen.keySet());
        final List<String> traducenos = new ArrayList<String>();
        for (final String llave : llaves)
        {
            traducenos.add(mapaOrigen.get(llave));
        }

        //Mandamos traducir
        final List<String> traducidos = traductor.traducir(traducenos, Lenguaje.desdeLocale(localeOrigen), Lenguaje.desdeLocale(localeDestino));

        //Generamos el mapa con las traducciones
        final Map<String, String> traducciones = new HashMap<String, String>();
        for (int i = 0; i < llaves.size(); i++)
        {
            final String llave = llaves.get(i);
            final String traduccion = traducidos.get(i);
            if (null == traduccion) break;
            traducciones.put(llave, traduccion.replace("(0)", "{0}")
                    .replace("(1)", "{1}")
                    .replace("(2)", "{2}")
                    .replace("(3)", "{3}")
                    .replace("(4)", "{4}")
                    .replace("(5)", "{5}")
                    .replace("(6)", "{6}")
                    .replace("(7)", "{7}")
                    .replace("(8)", "{8}")
                    .replace("(9)", "{9}"));
        }

        //Error al traducir el texto
        if (traducciones.isEmpty())
        {
            return null;
        }
        //Se pudo traducir el texto.
        else
        {
            final Properties properties = new Properties();
            final Set<String> llavesTraducidas = traducciones.keySet();
            for (final String llave : llavesTraducidas)
            {
                final String valor = traducciones.get(llave);
                traducciones.put(llave, valor);
                properties.setProperty(llave, valor);
            }

            //Almacenamos las propiedades
            final File archivoTraducciones = getFile(localeDestino);
            final FileOutputStream fos = new FileOutputStream(archivoTraducciones);
            properties.storeToXML(fos, m_nombreContexto + " traducido a " + localeDestino, "UTF8");
            fos.close();

            return new BundleTraducido(mapaOrigen, traducciones, archivoTraducciones);
        }
    }

    /**
     * Genera el nombre del archivo para  un {@link Locale}.
     *
     * @param locale para el que se genera el nombre de archivo.
     *
     * @return {@link File} archivo generado.
     */
    public File getFile(final Locale locale)
    {
        return new File(m_directorioTraduccion, m_nombreContexto + "_" + locale.getLanguage() + ".xml");
    }

    /**
     * Devuelve un mapa con todas las llaves y valores del {@link ResourceBundle}.
     * Es &uacute;til cuando no se quieren excepciones al no encontrar una llave.
     *
     * @param bundle a mapear.
     *
     * @return el {@link Map} creado.
     */
    private Map<String, String> mapear(final ResourceBundle bundle)
    {
        final Map<String, String> salida = new HashMap<String, String>();

        final Enumeration<String> keys = bundle.getKeys();
        while (keys.hasMoreElements())
        {
            final String key = keys.nextElement();
            salida.put(key, bundle.getString(key));
        }
        return salida;
    }

    /**
     * A&ntilde;ade un ClassLoader al listado m_loaders, que servir&aacute; para cargar el bundle.
     *
     * @param o {@link ClassLoader} a a&ntilde;adir.
     *
     * @return <tt>true</tt> (as per the general contract of the
     *         <tt>Collection.add</tt> method).
     */
    public boolean addClassLoader(ClassLoader o)
    {
        return m_loaders.add(o);
    }

    /**
     * Remueve un {@link ClassLoader} del listado <code>m_loaders</code>, que servir&aacute; para cargar el bundle.
     *
     * @param o {@link ClassLoader} a eliminar.
     *
     * @return <tt>true</tt> if this list contained the specified element.
     */
    public boolean removeClassLoader(ClassLoader o)
    {
        return m_loaders.remove(o);
    }

    /**
     * Devuelve una copia del listado <code>m_loaders</code>, que servir&aacute; para cargar el bundle.
     *
     * @return un nuevo ArrayList<ClassLoader>.
     */
    public List<ClassLoader> listClassLoaders()
    {
        return new ArrayList<ClassLoader>(m_loaders);
    }

    /**
     * Devuelve la Lista de los {@link ClassLoader} que servir&aacute; para cargar el bundle.
     *
     * @return List(unmodifiable) de 'm_loaders'.
     *
     * @see java.util.List
     */
    public List<ClassLoader> getClassLoaders()
    {
        return Collections.unmodifiableList(m_loaders);
    }

    /**
     * Establece los valores de la Lista de los {@link ClassLoader} que servir&aacute; para cargar el bundle.
     *
     * @param newList nuevo valor para 'm_loaders'. En caso de ser nulo se vaciar&aacute; el arreglo existente.
     *
     * @see java.util.List
     */
    public void setClassLoaders(List<? extends ClassLoader> newList)
    {
        this.m_loaders.clear();
        if (null != newList)
        {
            this.m_loaders.addAll(newList);
        }
    }

    /**
     * Nos dice si el {@link ClassLoader} se encuentra contenido en la
     * lista.
     *
     * @param o {@link ClassLoader} a buscar.
     *
     * @return <code>true</code> si lo encuentra; <code>false</code> en caso contrario.
     */
    public boolean containsClassLoader(ClassLoader o)
    {
        return m_loaders.contains(o);
    }

    /**
     * A&ntilde;ade un String al listado m_baseNamesBundles, que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     *
     * @param o {@link String} a a&ntilde;adir.
     *
     * @return <tt>true</tt> (as per the general contract of the
     *         <tt>Collection.add</tt> method).
     */
    public boolean addBaseName(String o)
    {
        return m_baseNamesBundles.add(o);
    }

    /**
     * Remueve un {@link String} del listado <code>m_baseNamesBundles</code>, que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     *
     * @param o {@link String} a eliminar.
     *
     * @return <tt>true</tt> if this list contained the specified element.
     */
    public boolean removeBaseName(String o)
    {
        return m_baseNamesBundles.remove(o);
    }

    /**
     * Devuelve una copia del listado <code>m_baseNamesBundles</code>, que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     *
     * @return un nuevo ArrayList<String>.
     */
    public List<String> listBaseName()
    {
        return new ArrayList<String>(m_baseNamesBundles);
    }

    /**
     * Devuelve la Lista de los {@link String} que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     *
     * @return List(unmodifiable) de 'm_baseNamesBundles'.
     *
     * @see java.util.List
     */
    public List<String> getBaseNames()
    {
        return Collections.unmodifiableList(m_baseNamesBundles);
    }

    /**
     * Establece los valores de la Lista de los {@link String} que servir&aacute;n para construir el {@link ResourceBundle} combinado.
     *
     * @param newList nuevo valor para 'm_baseNamesBundles'. En caso de ser nulo se vaciar&aacute; el arreglo existente.
     *
     * @see java.util.List
     */
    public void setBaseNames(List<? extends String> newList)
    {
        this.m_baseNamesBundles.clear();
        if (null != newList)
        {
            this.m_baseNamesBundles.addAll(newList);
        }
    }

    /**
     * Nos dice si el {@link String} se encuentra contenido en la
     * lista.
     *
     * @param o {@link String} a buscar.
     *
     * @return <code>true</code> si lo encuentra; <code>false</code> en caso contrario.
     */
    public boolean containsBaseNames(String o)
    {
        return m_baseNamesBundles.contains(o);
    }

    /**
     * Returns a string representation of the object. In general, the
     * {@link this#toString()} method returns a string that
     * "textually represents" this object. The result should
     * be a concise but informative representation that is easy for a
     * person to read.
     * It is recommended that all subclasses override this method.
     * <p/>
     * The {@link this#toString()} method for class {@link Object}
     * returns a string consisting of the name of the class of which the
     * object is an instance, the at-sign character `{@link @}', and
     * the unsigned hexadecimal representation of the hash code of the
     * object. In other words, this method returns a string equal to the
     * value of:
     * <blockquote>
     * <pre>
     * getClass().getName() + '@' + Integer.toHexString(hashCode())
     * </pre></blockquote>
     *
     * @return a string representation of the object.
     */
    @Override
    public String toString()
    {
        return m_nombreContexto + '@' + Integer.toHexString(hashCode());
    }
}